/**
 * 
 * sched.c
 * 
 * El scheduler utiliza un algoritmo round-robin con desalojo. 
 * 
 * Los procesos libres, es decir, los que no estan siendo utilizados, 
 * se almacenan en una lista circular doblemente enlazada de procesos 
 * libres.
 * 
 * Los procesos listos para ser ejecutados se almacenan en una lista 
 * circular doblemente enlazada de procesos listos.
 * 
 * Cuando un proceso llega al scheduler, el nodo asociado al PID del 
 * proceso es removido de la lista de procesos libres y agregado al 
 * final de la lista de procesos listos.
 * 
 * El proceso que esta corriendo actualmente no se encuentra en 
 * ninguna lista. 
 * 
 * Si este proceso agota su quantum, el mismo es agregado al final de 
 * la cola de procesos listos reseteando su quantum y el primer 
 * elemento de esta cola es removido de la misma y marcado como proceso 
 * actual. 
 * 
 * Si el proceso actual se bloquea, no se lo almacena en ninguna cola, 
 * sino que simplemente se lo abandona, a la espera de que en algun 
 * momento se pida desbloquearlo. Al bloquearse se marca como proceso 
 * actual al primer elemento de la cola de listos y se lo remueve de 
 * la misma. Cuando el proceso se desbloquea, el mismo es agregado al
 * final de la cola de procesos listos con el quantum restante que 
 * poseia al momento de bloquearse.
 * 
 * */

#include <idt.h>
#include <sched.h>
#include <loader.h>
#include <vga.h>
#include <debug.h>
#include <i386.h>

#define DEFAULT_QUANTUM 10

/**
 * Nodo para las distintas listas de tareas.
 * Notar que cada nodo pertenece en cada momento 
 * a una sola lista **/

typedef struct str_sched_task sched_task;
struct str_sched_task
{
    uint_32     quantum;
    uint_32     rem_quantum;
    
    sched_task *next_task;
    sched_task *prev_task;
};

/* Nodos de tareas */
sched_task  tasks[MAX_PID] = { {0, 0, NULL, NULL} };

/* Cola circular de tareas listas para ser ejecutadas */
sched_task *ready_queue;

/* Cola circular de tareas bloqueadas */
sched_task *bloqued_queue;

/* Cola circular de tareas libres */
sched_task *free_queue;

/* Tarea actual */
sched_task *current_task;

static sint_32 sched_get_pid(sched_task *task_ptr);
static void sched_add_to_queue(sched_task *task_ptr, sched_task **queue);
static void sched_rem_from_queue(sched_task *task_ptr, sched_task **queue);

static void sched_debug_queue(sched_task **queue);

/**
 * Inicializacion de las estructuras del scheduler
 * 
 **/

void sched_init()
{
    current_task = ready_queue = bloqued_queue = free_queue = NULL;
    
    int i;
    for(i = 0; i < MAX_PID; i++)
        sched_add_to_queue(tasks + i, &free_queue);
}

/**
 * Carga de una tarea en el scheduler. La tarea se agrega como 
 * lista para ser ejecutada y con el quantum por defecto.
 * 
 * @param proc_id
 * 
 * PID del proceso a cargar en el scheduler.
 * 
 **/

void sched_load(pid proc_id)
{
    sched_task *new_task = tasks + proc_id;
    
    new_task->quantum = DEFAULT_QUANTUM;
    new_task->rem_quantum = DEFAULT_QUANTUM;
    
    sched_rem_from_queue(new_task, &free_queue);
    sched_add_to_queue(new_task, &ready_queue);
}

/**
 * Saca al proceso de la cola de bloqueados y lo agrega al final de 
 * la cola de procesos listos.
 * 
 * @param proc_id
 * 
 * PID del proceso a desbloquear.
 * 
 **/
 
void sched_unblock(pid proc_id)
{
    sched_task *unbloqued_task = tasks + proc_id;
    
    // sched_rem_from_queue(unbloqued_task, &bloqued_queue);
    
    sched_add_to_queue(unbloqued_task, &ready_queue);
}

/**
 * Elimina al proceso que esta corriendo actualmente del scheduler y 
 * lo agrega a la cola de procesos libres.
 * 
 * @return
 * 
 * PID del siguiente proceso a ejecutar.
 * 
 **/

int sched_exit()
{
    sched_add_to_queue(current_task, &free_queue);
    
    sched_task *next_task = ready_queue;
    
    if (!next_task)
        debug_kernelpanic_custom("sched_exit(): No hay tareas listas para ejecutar!");
        
    sched_rem_from_queue(next_task, &ready_queue);
    
    current_task = next_task;
    
    return sched_get_pid(current_task);
}

/**
 * Bloquea al proceso que esta corriendo actualmente y setea como 
 * proceso actual al primero de la cola de procesos listos.
 * 
 * @return
 * 
 * PID del siguiente proceso a ejecutar.
 * 
 **/

int sched_block()
{
    // sched_add_to_queue(current_task, &bloqued_queue);
    
    sched_task *next_task = ready_queue;
    
    if (!next_task)
        debug_kernelpanic_custom("sched_block(): No hay tareas listas para ejecutar!");
        
    sched_rem_from_queue(next_task, &ready_queue);
    
    current_task = next_task;
    
    return sched_get_pid(current_task);
}

int sched_tick()
{
    vga_printf("sched_tick(): Tarea al llegar %d\n", sched_get_pid(current_task));

    if (--(current_task->rem_quantum) == 0)
    {
        vga_printf("sched_tick(): Cambio de tarea!\n");
        
        current_task->rem_quantum = current_task->quantum;
        
        sched_add_to_queue(current_task, &ready_queue);
        current_task = ready_queue;
        sched_rem_from_queue(current_task, &ready_queue);
    }

    vga_printf("sched_tick(): Tarea al salir %d\n", sched_get_pid(current_task));

    return sched_get_pid(current_task);
}

/**
 * Funciones de debug para las distintas colas del scheduler
 * 
 **/

void sched_debug_ready_queue()
{
    vga_printf("Debug de procesos listos...\n");
    
    sched_debug_queue(&ready_queue);
}

void sched_debug_free_queue()
{
    vga_printf("Debug de procesos libres...\n");
    
    sched_debug_queue(&free_queue);
}

/**********************************************************************/
/***                   FUNCIONES NO EXPORTADAS                      ***/
/**********************************************************************/

static sint_32 sched_get_pid(sched_task *task_ptr)
{
    return (sint_32) (task_ptr - tasks);
}

/**
 * Agrega el nodo pasado como parametro al final de la cola indicada.
 * 
 * @param task_ptr
 * 
 * Puntero al nodo a agregar en la cola.
 * 
 * @param queue
 * 
 * Puntero a la cola donde agregar el nodo.
 * 
 **/

static void sched_add_to_queue(sched_task *task_ptr, sched_task **queue)
{
    /* Recordar que es una cola circular */
    
    if (!*queue) /* La primera tarea */
    {
        *queue = task_ptr;
        task_ptr->next_task = task_ptr;
        task_ptr->prev_task = task_ptr;
    }
    else /* Hay al menos una tarea en la cola, agrego la nueva al final */
    {
        sched_task *older_last = (*queue)->prev_task;
        
        task_ptr->next_task = *queue;
        task_ptr->prev_task = older_last;
        
        older_last->next_task = task_ptr;
        
        (*queue)->prev_task = task_ptr;
    }
}

/**
 * Remueve el nodo pasado como parametro de la cola indicada.
 * 
 * @param task_ptr
 * 
 * Puntero al nodo que se quiere eliminar de la cola.
 * 
 * @param queue
 * 
 * Puntero a la cola que contiene el nodo indicado.
 * 
 **/

static void sched_rem_from_queue(sched_task *task_ptr, sched_task **queue)
{
    /* Recordar que es una cola circular */
    
    if (*queue == task_ptr) /* Tengo que sacar el primero */
    {
        if (task_ptr->next_task == task_ptr) /* Hay solo un elemento */
        {
            *(queue) = NULL;
        }
        else /* Hay al menos 2 */
        {
            sched_task *last_node = (*queue)->prev_task;
            sched_task *new_first = (*queue)->next_task;
            
            *(queue) = new_first;
            
            last_node->next_task = new_first;
            new_first->prev_task = last_node;
        }
    }
    else /* No tengo que sacar el primero */
    {
        sched_task *prev_node = task_ptr->prev_task;
        sched_task *next_node = task_ptr->next_task;
        
        prev_node->next_task = next_node;
        next_node->prev_task = prev_node;
    }
    
    task_ptr->next_task = task_ptr->prev_task = NULL;
}

/**
 * Funcion de debug
 * 
 **/

static void sched_debug_queue(sched_task **queue)
{
    if (!*queue)
    {
        vga_printf(" - No hay procesos en la lista!\n");
    }
    else
    {
    
        bool pase;
        sched_task *task_ptr;
        for
        (
            task_ptr  = *queue, pase = FALSE;
            task_ptr != *queue || !pase;
            task_ptr  = task_ptr->next_task, pase = TRUE
        )
        {
            vga_printf(" - Proceso %d: Anterior: %d, Siguiente: %d\n",
                sched_get_pid(task_ptr),
                sched_get_pid(task_ptr->prev_task),
                sched_get_pid(task_ptr->next_task));
        }
    }
}
